-
Notifications
You must be signed in to change notification settings - Fork 123
Add new HTTPConnectionPool #418
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
48dd7a3
to
3c96411
Compare
3c96411
to
7b4da91
Compare
9beffc7
to
4011291
Compare
3aba015
to
5d707f7
Compare
case .executeRequest(let request, let connection, cancelTimeout: let cancelTimeout): | ||
connection.executeRequest(request.req) | ||
if cancelTimeout { | ||
self.cancelRequestTimeout(request.id) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should these two operations be in the other order?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added a code comment.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I disagree with your code comment. If we're about to execute a request then we should aim to cancel the timeout first, as this will make some small number of requests succeed where the other order would fail.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's look at the definition of the request timeouts.
When a request timeout fires, it will tell the state machine about it having fired. The state machine will then look for the request in its own request queue. If it doesn't find the request in there, it knows that the request was already removed (probably in another thread) before. The action will for this reason be none.
For this reason the order in calling the request actions does not matter. The only place where we make decisions about failing requests is in the state machine.
From the HTTPConnectionPool
:
private func scheduleRequestTimeout(_ request: Request, on eventLoop: EventLoop) {
let requestID = request.id
let scheduled = eventLoop.scheduleTask(deadline: request.connectionDeadline) {
// The timer has fired. Now we need to do a couple of things:
//
// 1. Remove ourselves from the timer dictionary to not leak any data. If our
// waiter entry still exists, we need to tell the state machine, that we want
// to fail the request.
let timeoutFired = self.timerLock.withLock {
self._requestTimer.removeValue(forKey: requestID) != nil
}
// 2. If the entry did not exists anymore, we can assume that the request was
// scheduled on another connection. The timer still fired anyhow because of a
// race. In such a situation we don't need to do anything.
guard timeoutFired else { return }
// 3. Tell the state machine about the timeout
let action = self.stateLock.withLock {
self._state.timeoutRequest(requestID)
}
self.run(action: action)
}
self.timerLock.withLockVoid {
assert(self._requestTimer[requestID] == nil)
self._requestTimer[requestID] = scheduled
}
request.req.requestWasQueued(self)
}
From the StateMachine:
mutating func timeoutRequest(_ requestID: Request.ID) -> Action {
// 1. check requests in queue
if let request = self.requests.remove(requestID) {
return .init(
request: .failRequest(request, HTTPClientError.getConnectionFromPoolTimeout, cancelTimeout: false),
connection: .none
)
}
// 2. This point is reached, because the request may have already been scheduled. A
// connection might have become available shortly before the request timeout timer
// fired.
return .none
}
case .executeRequestsAndCancelTimeouts(let requests, let connection): | ||
for request in requests { | ||
connection.executeRequest(request.req) | ||
self.cancelRequestTimeout(request.id) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same ordering question here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For efficiency reasons should we be able to do a "bulk" request timeout cancellation? We have to acquire locks in order to do this (both the timerLock
and the event loop locks) so it may be useful to try to coalesce work where we can to try to reduce the pressure on those locks.
connectionID: connectionID, | ||
http1ConnectionDelegate: self, | ||
http2ConnectionDelegate: self, | ||
deadline: .now() + (self.clientConfiguration.timeout.connect ?? .seconds(30)), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can this .seconds(30)
be lifted out to a static
so that we can see it clearly and give it a name?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is coming together nicely! It's quite mechanical so it's reasonably easy to review as well.
func connectionPoolDidShutdown(_ pool: HTTPConnectionPool, unclean: Bool) | ||
} | ||
|
||
class HTTPConnectionPool { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
class HTTPConnectionPool { | |
final class HTTPConnectionPool { |
@@ -121,6 +128,364 @@ enum HTTPConnectionPool { | |||
} | |||
} | |||
} | |||
|
|||
let stateLock = Lock() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
private?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not actioned.
} | ||
} | ||
|
||
let timerLock = Lock() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
private?
|
||
case .closeConnection(let connection, isShutdown: let isShutdown): | ||
// we are not interested in the close future... | ||
_ = connection.close() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we add an equivalent close API accepting an optional promise and use that instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great idea. Promise is enough I guess :)
self.run(action: action) | ||
} | ||
|
||
func run(action: StateMachine.Action) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
private?
func runRequestAction(_ action: StateMachine.RequestAction) { | ||
switch action { | ||
case .executeRequest(let request, let connection, cancelTimeout: let cancelTimeout): | ||
connection.executeRequest(request.req) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is the ordering of execute and cancelling the timeout done for any particular reason?
// MARK: Run actions | ||
|
||
private func createConnection(_ connectionID: Connection.ID, on eventLoop: EventLoop) { | ||
self.connectionFactory.makeConnection( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This API wasn't too clear to me. In my mind "makeThing" should return a "Thing" to the caller. Consider a different name / dropping a comment in here.
// The timer has fired. Now we need to do a couple of things: | ||
// | ||
// 1. Remove ourselves from the timer dictionary to not leak any data. If our | ||
// waiter entry still exist, we need to tell the state machine, that we want |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// waiter entry still exist, we need to tell the state machine, that we want | |
// waiter entry still exists, we need to tell the state machine, that we want |
// waiter entry still exist, we need to tell the state machine, that we want | ||
// to fail the request. | ||
|
||
let timeout = self.timerLock.withLock { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: timeoutFired
/ timedOut
5cefa51
to
40b6851
Compare
40b6851
to
25d23f4
Compare
Co-authored-by: George Barnett <[email protected]>
25d23f4
to
ff70dd2
Compare
@@ -121,6 +128,364 @@ enum HTTPConnectionPool { | |||
} | |||
} | |||
} | |||
|
|||
let stateLock = Lock() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not actioned.
} | ||
} | ||
|
||
static let fallbackConnectTimeout: TimeAmount = .seconds(30) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
private
here too.
case .executeRequest(let request, let connection, cancelTimeout: let cancelTimeout): | ||
connection.executeRequest(request.req) | ||
if cancelTimeout { | ||
self.cancelRequestTimeout(request.id) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I disagree with your code comment. If we're about to execute a request then we should aim to cancel the timeout first, as this will make some small number of requests succeed where the other order would fail.
|
||
private func createConnection(_ connectionID: Connection.ID, on eventLoop: EventLoop) { | ||
// Even though this function is called make it actually creates/establishes a connection. | ||
// TBD: Should we rename it? To what? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe just changing "make" to "create" is enough to differentiate make it clear that it's not a factory. Alternatively something like provideNewConnection(to: self, ...)
?
Superseeded by #420 |
Follow up to #416